
import re
import sys
import datetime
from threading import local

from django.db.backends import *
from mongomodel.mongodb.creation import DatabaseCreation

import decimal
import pymongo
from pymongo.code import Code


class DatabaseIntrospection(BaseDatabaseIntrospection):

    def table_names(self):
        "Returns a list of names of all tables that exist in the database."
        self.connection.cursor()
        return self.connection.database.collection_names()

class DatabaseClient(BaseDatabaseClient):
    executable_name = 'mongodb'

    def runshell(self):
        args = [self.executable_name,
                self.connection.settings_dict['NAME']]
        if os.name == 'nt':
            sys.exit(os.system(" ".join(args)))
        else:
            os.execvp(self.executable_name, args)

class DatabaseFeatures(BaseDatabaseFeatures):
    # SQLite cannot handle us only partially reading from a cursor's result set
    # and then writing the same rows to the database in another cursor. This
    # setting ensures we always read result sets fully into memory all in one
    # go.
    can_use_chunked_reads = False
    #allows_group_by_pk = False
    # True if django.db.backend.utils.typecast_timestamp is used on values
    # returned from dates() calls.
    needs_datetime_string_cast = False
    allows_group_by_pk = True
    #empty_fetchmany_value = []
    #update_can_self_select = True
    #interprets_empty_strings_as_nulls = False
    #can_use_chunked_reads = True
    can_return_id_from_insert = False
    #uses_autocommit = False
    #uses_savepoints = False
    # If True, don't use integer foreign keys referring to, e.g., positive
    # integer primary keys.
    #related_fields_match_type = False
    #allow_sliced_subqueries = True


class DatabaseOperations(BaseDatabaseOperations):

    compiler_module = "mongomodel.mongodb.compiler"

    def lookup(self, lookup_type, value ):

        raise NotImplementedError
        func = self.lookups.get(lookup_type)
        if func:
            return func(value)


    def date_extract_sql(self, lookup_type, field_name):
        # sqlite doesn't support extract, so we fake it with the user-defined
        # function django_extract that's registered in connect(). Note that
        # single quotes are used because this is a string (and could otherwise
        # cause a collision with a field name).
        return "django_extract('%s', %s)" % (lookup_type.lower(), field_name)

    def date_trunc_sql(self, lookup_type, field_name):
        # sqlite doesn't support DATE_TRUNC, so we fake it with a user-defined
        # function django_date_trunc that's registered in connect(). Note that
        # single quotes are used because this is a string (and could otherwise
        # cause a collision with a field name).
        return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name)

    def drop_foreignkey_sql(self):
        return ""

    def pk_default_value(self):
        return 'NULL'

    def quote_name(self, name):
        if name.startswith('"') and name.endswith('"'):
            return name # Quoting once is enough.
        return '"%s"' % name

    def no_limit_value(self):
        return -1

    def sql_flush(self, style, tables, sequences):
        raise NotImplementedError

    def year_lookup_bounds(self, value):
        first = datetime.datetime( int(value), 1,1 )
        second = datetime.datetime( int(value), 12,31, 23,59,59,999999 )
        return [first,second]

    def convert_values(self, value, field):
        """SQLite returns floats when it should be returning decimals,
        and gets dates and datetimes wrong.
        For consistency with other backends, coerce when required.
        """
        if value is None:
            return None
        internal_type = field.get_internal_type()
        if internal_type == 'DecimalField':
            #return util.typecast_decimal(format_number(value))
            return decimal.Decimal(value)
        elif internal_type and internal_type.endswith('IntegerField') : # or internal_type == 'AutoField':
            return int(value)
        elif internal_type == 'DateField':
            #return util.typecast_date(value)
            return datetime.date( value.year, value.month, value.day )
        #elif internal_type == 'DateTimeField':
        #    return util.typecast_timestamp(value)
        elif internal_type == 'TimeField':
            return typecast_time(value)
        elif internal_type == 'DocumentField':
            return field.doc(**dict( [(str(k), self.convert_values(v, field.doc._meta.get_field_by_name(k)[0])) for k,v in value.items() ] ))
        elif internal_type == 'DocumentListField':
            value_list = []
            for item in value:
                doc= field.doc(**dict( [(str(k), self.convert_values(v, field.doc._meta.get_field_by_name(k)[0])) for k,v in item.items() ] ))
                value_list.append(doc)

            return value_list
        elif internal_type == 'ListField':
            value_list = [ self.convert_values(v, field.field) for v in value] 

            return value_list

        # No field, or the field isn't known to be a decimal or integer
        return value

    def value_to_db_datetime(self, value):
        """
        Transform a datetime value to an object compatible with what is expected
        by the backend driver for datetime columns.
        """
        if value is None:
            return None
        return value

    def value_to_pk(self, value):
        if value is None:
            return None
        if isinstance(value, (str,unicode) ):
            return pymongo.objectid.ObjectId(value)
        return value

    def value_to_db_date(self, value):
        """
        Transform a date value to an object compatible with what is expected
        by the backend driver for date columns.
        """
        if value is None:
            return None
        return datetime.datetime( value.year, value.month, value.day )

    def value_to_db_decimal(self, value, max_digits, decimal_places):
        """
        Transform a decimal.Decimal value to an object compatible with what is
        expected by the backend driver for decimal (numeric) columns.
        """
        if value is None:
            return None
        return format_number(value, max_digits, decimal_places)

class DatabaseWrapper(BaseDatabaseWrapper):

    # SQLite requires LIKE statements to include an ESCAPE clause if the value
    # being escaped has a percent or underscore in it.
    # See http://www.sqlite.org/lang_expr.html for an explanation.
    between_op = lambda v : {'$gte' : v[0], '$lte' : v[1] } 
    negated_between_op = lambda v : { '$or' : [{'$lt' : v[0]}, {'$gt' : v[1] }] }
    operators = {
        'exact': lambda v : v[0],
        'year' : between_op,
        #'range' : 'range %s',
        #'iexact': "like %s ESCAPE '\\'",
        #'contains': "like %s ESCAPE '\\'",
        'iexact': lambda v : re.compile('^%s$' % v[0], re.I) ,
        'contains': lambda v : re.compile('%s' % v[0]) ,
        'icontains': lambda v : re.compile('%s' % v[0], re.I) ,
        #'regex': 'regexp %s',
        #'iregex': "regexp '(?i)' || %s",
        'gt': lambda v: {'$gt' : v[0] }, 
        'gte': lambda v: {'$gte' : v[0] }, 
        'lt': lambda v: {'$lt' : v[0] }, 
        'lte': lambda v: {'$lte' : v[0] }, 
        'startswith': lambda v : re.compile('^%s' % v[0].replace('%','')) ,
        'istartswith': lambda v : re.compile('^%s' % v[0].replace('%',''), re.I) ,
        'endswith': lambda v : re.compile('%s$' % v[0].replace('%','')) ,
        'iendswith': lambda v : re.compile('%s$' % v[0].replace('%',''), re.I) ,
        #'endswith': 'endswith %s',
        #'iendswith': 'iendswith %s',
        'in': lambda v: {'$in' : v },
    }
    negated_operators = {
        'exact': lambda v : {'$ne': v[0]},
        'year' : negated_between_op,
        #'range' : 'range %s',
        #'iexact': "like %s ESCAPE '\\'",
        'iexact': lambda v : {'$not': re.compile('^%s$' % v[0], re.I)} ,
        'contains': lambda v : {'$not': re.compile('%s' % v[0]) },
        'icontains': lambda v : {'$not': re.compile('%s' % v[0], re.I) },
        #'regex': 'regexp %s',
        #'iregex': "regexp '(?i)' || %s",
        'gt': lambda v: {'$lte' : v[0] }, 
        'gte': lambda v: {'$lt' : v[0] }, 
        'lt': lambda v: {'$gte' : v[0] }, 
        'lte': lambda v: {'$gt' : v[0] }, 
        'startswith': lambda v : {'$not':re.compile('^%s' % v[0].replace('%',''))} ,
        'istartswith': lambda v : {'$not':re.compile('^%s' % v[0].replace('%',''),re.I)} ,
        'endswith': lambda v : {'$not':re.compile('%s$' % v[0].replace('%',''))} ,
        'iendswith': lambda v : {'$not':re.compile('%s$' % v[0].replace('%',''),re.I)} ,
        #'endswith': 'endswith %s',
        #'iendswith': 'iendswith %s',
        'in': lambda v: {'$nin' : v },
    }

    def __init__(self, *args, **kwargs):
        super(DatabaseWrapper, self).__init__(*args, **kwargs)

        self.features = DatabaseFeatures()
        self.ops = DatabaseOperations()
        self.client = DatabaseClient(self)
        self.creation = DatabaseCreation(self)
        self.introspection = DatabaseIntrospection(self)
        self.validation = BaseDatabaseValidation(self)

        self.database = None

    def _commit(self):
        if self.connection is not None:
            return True

    def _rollback(self):
        if self.connection is not None:
            return True

    def reconnect(self):
        self.connection = None
        self._cursor()

    def _cursor(self):
        if self.connection is None:
            settings_dict = self.settings_dict
            if not settings_dict['NAME']:
                from django.core.exceptions import ImproperlyConfigured
                raise ImproperlyConfigured("Please fill out the database NAME in the settings module before using the database.")
            kwargs = {
                'database': settings_dict['NAME'],
                #'detect_types': Database.PARSE_DECLTYPES | Database.PARSE_COLNAMES,
            }
            kwargs.update(settings_dict['OPTIONS'])
            if not settings_dict['HOST']:
                settings_dict['HOST'] = 'localhost'
            if not settings_dict['PORT']:
                settings_dict['PORT'] = 27017

            self.connection = pymongo.Connection(settings_dict['HOST'],int(settings_dict['PORT']))
            self.database = self.connection[ settings_dict['NAME'] ]
        return MongodbCursorWrapper(self)

    def close(self):
        pass


class MongodbCursorWrapper(object):
    """
    Django uses "format" style placeholders, but pysqlite2 uses "qmark" style.
    This fixes it -- but note that if you want to use a literal "%s" in a query,
    you'll need to use "%%s".
    """

    DEBUG = False

    def __init__(self, conn):

        self.init(conn)
        self.cursor = None
        self.__pos = 0
        self.lastrowid = None
        self.affected = 0

    def init(self, conn):
        self.connection = conn
        self.database = conn.database

    def insert(self, table, query ):

        if self.DEBUG:
            print 'INSERT', table, query

        collection = self.database[table]
        self.lastrowid = collection.insert(query , safe=True)

        return self.lastrowid 

    def find_one(self, table, query, options={} ):

        if self.DEBUG:
            print 'FINDONE', table, query, options

        collection = self.database[table]

        doc = collection.find_one(query,**options)
        return doc
        
    def find(self, table, query, options={} ):

        if self.DEBUG:
            print 'FIND', table, query, options

        self.__pos = 0
        collection = self.database[table]

        self.cursor = collection.find(query,**options)
        return self

    def avg(self, table, col, query, options={} ):

        options['initial'] = { 'total' : 0.0, 'count': 0 }
        options['reduce'] = "function(obj,prev) { if( obj.%s ) {prev.total += obj.%s;} prev.count++; }" % (col,col)
        options['finalize'] = "function(out){ out.avg = out.total / out.count }"
        
        result = self.group(table, query,options)
        return result[0]['avg']

    def sum(self, table, col, query, options={} ):

        options['initial'] = { 'total' : 0.0, 'count': 0 }
        options['reduce'] = "function(obj,prev) { if( obj.%s ) {prev.total += obj.%s;} prev.count++; }" % (col,col)
        
        result = self.group(table, query,options)
        return result[0]['total']

    def max(self, table, col, query, options={},method='Math.max' ):

        m = Code("""
        function () {
            if (this.%(col)s != null) {
                emit( "%(col)s", this.%(col)s );
            }
        }
        """ % {'col':col})
        r = Code("""
        function (key, values) {

            if (values.length == 0) return null;
            var result = values[0];
            for (var i=1; i< values.length; i++ ) {
                result = %s( result, values[i] );
            }
            return result;
        }
        """ % method)
        result = self.map_reduce(table, m,r,query, options)
        return result[0]['value']

    def min(self, table, col, query, options={} ):

        return self.max( table, col, query, options, 'Math.min')


    def group(self, table, query, options={} ):

        options.pop('sort',None)
        if self.DEBUG:
            print 'GROUP', table, query, options

        key = {}
        for f in options.pop('fields',[] ) or []:
            key[f] = 1

        collection = self.database[table]

        result = collection.group(key,query,**options)
        return result

    def map_reduce(self, table, m,r, query, options={} ):

        if query:
            options['query'] = query
        if self.DEBUG:
            print 'MAP_REDUCE', table, query,m,r, options

        collection = self.database[table]

        result = collection.map_reduce(m,r,**options).find()
        #print options,result
        return result

    def __iter__(self):
        return self

    def reconnect(self):
        self.connection.reconnect()
        self.init( self.connection )

    def auto_reconnect( f ):
        def new_f(*args, **kwds):
            try:
                result = f(*args, **kwds)
            except pymongo.errors.AutoReconnect:
                args[0].reconnect()
                result = f(*args, **kwds)

            return result
        new_f.func_name = f.func_name
        return new_f

    @auto_reconnect
    def next(self):
        row = self.cursor.next()

        return row

    def __getitem__(self, idx):
        return self.cursor[idx]

    def update(self, table, query, options={} ):

        if self.DEBUG:
            print 'UPDATE', table, query, options

        collection = self.database[table]
        err= collection.update(query, {'$set': options}, safe=True )
        if not err['err']:
            self.affected = err['n']
            return self.affected
        else:
            raise err['err']

    def delete(self, table, query  ):

        if self.DEBUG:
            print 'DELETE', table, query

        collection = self.database[table]
        err= collection.remove(query, safe=True )
        if not err['err']:
            self.affected = err['n']
            return self.affected
        else:
            raise err['err']

    def execute(self, query, options={} ):



        if isinstance( query , tuple ):
            table, method, args, kwargs = query
            if self.DEBUG:
                print 'EXCUTE', table, method, args, kwargs
            collection = self.database[table]
            m = getattr( collection, method )
            return m( *args, **kwargs )

        raise NotImplementedError
        self.__pos = 0
        collection = self.database[table]
        cursor = None

        if command == 'select':
            cursor= collection.find(query,**options)
            self.cursor = cursor
        elif command == 'insert':
            cursor= collection.insert(query )
            self.lastrowid = cursor
        elif command == 'update':
            err= collection.update(query,options, safe=True )
            if not err['err']:
                self.affected = err['n']
        elif command == 'delete':
            err= collection.remove(query, safe=True )
            if not err['err']:
                self.affected = err['n']

        return cursor



        #try:
        #    return Database.Cursor.execute(self, query, params)
        #except Database.IntegrityError, e:
        #    raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2]
        #except Database.DatabaseError, e:
        #    raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2]

    def executemany(self, query, param_list):
        query = self.convert_query(query)
        try:
            return Database.Cursor.executemany(self, query, param_list)
        except Database.IntegrityError, e:
            raise utils.IntegrityError, utils.IntegrityError(*tuple(e)), sys.exc_info()[2]
        except Database.DatabaseError, e:
            raise utils.DatabaseError, utils.DatabaseError(*tuple(e)), sys.exc_info()[2]


    def fetchmany(self, size):

        result = []
        count = self.cursor.count()
        try:
            for i in xrange(min(count-self.__pos,self.__pos+size)):
                result.append( self.cursor[self.__pos] )
                self.__pos += 1
        except IndexError:
            pass
        return result

    def fetchone(self):
        return self.cursor[0]

    def count(self):

        if not self.cursor:
            return self.affected
        return self.cursor.count()

    rowcount = property(count)

def _sqlite_extract(lookup_type, dt):
    if dt is None:
        return None
    try:
        dt = util.typecast_timestamp(dt)
    except (ValueError, TypeError):
        return None
    if lookup_type == 'week_day':
        return (dt.isoweekday() % 7) + 1
    else:
        return getattr(dt, lookup_type)

def _sqlite_date_trunc(lookup_type, dt):
    try:
        dt = util.typecast_timestamp(dt)
    except (ValueError, TypeError):
        return None
    if lookup_type == 'year':
        return "%i-01-01 00:00:00" % dt.year
    elif lookup_type == 'month':
        return "%i-%02i-01 00:00:00" % (dt.year, dt.month)
    elif lookup_type == 'day':
        return "%i-%02i-%02i 00:00:00" % (dt.year, dt.month, dt.day)

def _sqlite_regexp(re_pattern, re_string):
    import re
    try:
        return bool(re.search(re_pattern, re_string))
    except:
        return False

def format_number(value, max_digits, decimal_places):
    """
    Formats a number into a string with the requisite number of digits and
    decimal places.
    """
    if isinstance(value, decimal.Decimal):
        context = decimal.getcontext().copy()
        context.prec = max_digits
        return u'%s' % str(value.quantize(decimal.Decimal(".1") ** decimal_places, context=context))
    else:
        return u"%.*f" % (decimal_places, value)

def typecast_time(s): # does NOT store time zone information
    if not s: return None
    hour, minutes, seconds = s.split(':')
    if '.' in seconds: # check whether seconds have a fractional part
        seconds, microseconds = seconds.split('.')
    else:
        microseconds = '0'
    return datetime.time(int(hour), int(minutes), int(seconds), int(float('.'+microseconds) * 1000000))

